记一次 TCP 与 UDP 间的技术决策

背景

我最近在开发 InputShare 的付费版本,在开发过程中,一直收到内测用户反馈的鼠标定位不准的问题,具体表现为鼠标在安卓设备端时不时会大幅度移动一下。我尝试用 AI 改了好几版的通信逻辑都不行。

InputShare 是我开发的一个开源项目,旨在让电脑于安卓平板/手机共用同一套键盘鼠标。

InputShare 是什么?

最开始如何优化?

最开始的逻辑很简单,鼠标事件监听,收到一个移动事件就发送一个。这个实现起来简单,但是问题也很明显:如果用户使用的鼠标的回报率(或者叫轮询率)较高,事件发送的频率会非常高,按鼠标的不同,最高能达到数千赫兹。

于是我后来修改了逻辑,核心思想就是在监听移动事件之外另起一个线程,在这个线程中尽可能地以固定间隔向发送线程中推事件,这样可以统一程序在不同鼠标回报率下的行为,使用同样的发送速率。这个版本已经会好一些了,但用户反馈还是不太行。

后来如何决策改为使用 UDP?

我先调研了一些相似的开源项目,如 DeskflowBarrier 等,又使用 Grok 进行了调研,结果如下:

主流开源项目在涉及无线控制(例如远程桌面、键盘鼠标分享等场景)并需要发送和接收鼠标移动事件时,主要使用 TCP 协议。这是因为鼠标事件(如相对移动、点击)需要可靠传输,以避免包丢失导致的控制不准或延迟问题,尤其在无线网络环境下。UDP 虽然能提供更低的延迟,但通常仅在特定低延迟场景(如游戏或实验项目)中使用,且往往结合重传机制或作为辅助。以下基于调研的总结和示例(扩展到无线鼠标/键盘分享和远程控制项目,不限于 Android):

主要开源项目示例

UDP 的使用情况

UDP 在主流项目中较少见,主要出现在小众或特定优化场景:

总体上,TCP 是主流选择,因为无线网络易受干扰,鼠标事件的可靠性(如连续移动不丢帧)优先于极致低延迟。如果项目针对游戏或高帧率场景,UDP 可能更合适,但需自定义错误处理。在开源社区讨论中(如 GitHub 和 Reddit),TCP 被视为默认标准,以简化开发和确保跨平台兼容。

Grok 调研结果

总结就是主流开源项目都使用 TCP 实现。

然后再总结了针对项目中鼠标移动事件通信的特点:

这样看似乎使用 UDP 会更优?于是我尝试使用 Wireshark 进行抓包,结果如下:

TCP 下传输鼠标事件的 RTT 图表
TCP Time Sequence 图表

从图中可以看出,大部分时候下的 RTT 其实是符合我的应用需求的(我的应用需要每秒发送 150 次左右包,也就是 6.67 毫秒发送一次,图中的 RTT 平均下来是 8 毫秒左右,单程通信就是 4 毫秒,还不是性能瓶颈)。关键在于个别延迟到达的包(也就是 RTT 图中的突出点),由于 TCP 的有序性,在一个包延迟到达或者丢包时,接收端会等待这个包到达后再接收之后的包,而这恰恰是我的应用所不需要的。

为什么一开始使用 TCP?

主要是因为我的项目基于 scrcpy 这个项目的服务端部分进行实现,而这个项目本身是完全不使用 UDP 来通信的。

动手实践

在得出结论之后,我决定动手实践,fork 了 scrcpy,在创建服务端 socket 时额外创建了一个 UDP socket,专门用于监听 UHID 鼠标事件。

由于 UDP 不保证包到达的有序性,我在发送端额外给包带上了一个序号,并在接收端解析和检查序号,当收到的包的序号小于当前收到的最大序号时,直接无视。

在实现 demo 版本后,我马上打包发给用户测试,得到的反馈很好,之前提到的鼠标定位不准的问题也没有再出现。

于是我继续完善这个 demo,主要集中在 UDP 服务的创建和发送上。

demo 版本的 UDP 服务使用固定端口,虽然安卓设备上基本不会有创建 UDP 服务的需求,但是毕竟是做付费软件,还是得保证一下程序的鲁棒性。我后来在实现时依然使用固定端口,但是在绑定端口时会继续检查,如果端口被占用,则进行顺延,同时实现了一个响应 ping 信号的功能。在客户端侧,在需要连接到 UDP 服务时,使用 ping 信号来检查指定端口是否存在服务,如果不存在则同样进行顺延,如果完全扫不到端口则回退到使用 TCP 通信。

点此查看原文